package jam.network;

import jam.network.message.*;
import jam.model.*;

import java.io.*;
import java.net.*;
import java.util.*;

//
// =============================================================================
//

/**
 * Represents one single connected user.
 */
public class ClientController extends Thread {   
    private final long TIMEOUT = 120000;
	
    private NetworkController networkController;
    private ClientModel model;
    
    private Socket socket;
    private PrintWriter out;
    private Scanner in;
    
    private Map<Integer, ServerCommand> awaiting;
    private int nextTransactionId; 
    
    private Timer timeoutTimer; 
    private boolean ponged;
    
    //
    // =BREAKING=EVERYTHING====================================================
    //

    /**
     * Sends a message over the socket
     * @param message The message to be sent.
     */
    public void send(Message message) {
        System.out.println(message.toString());
        this.out.println(message.toString());
    }
    
    /**
     * Sends a message over the socket and puts it in the wait queue.
     * @param message A server message to be sent.
     */
    public void send(ServerCommand message) {  
        System.out.println(message.toString());
        this.awaiting.put(message.getTransactionId(), message);
        this.out.println(message.toString());
    }

    /**
     * Pulls an awaiting command from the map and then removes it.
     * @param transactionId The matching transaction ID
     * @return A server command that was waiting.
     */
    public ServerCommand removeAwaiting(int transactionId) {
        return this.awaiting.remove(transactionId);
    }
    
    /**
     * Disconnects this client from the network, sending messages as required
     */
    public void disconnect() {
        if (this.model.getUser() != null) {      	
        	NetworkController network = this.getNetworkController();
            ServerModel server = network.getModel();
            ClientModel client = this.getModel();    
            
            String username = client.getUser().getUsername();
            Collection<Channel> channels = client.getChannels();
                   
            // Loop through the channels and update as required.
            for (Channel channel: channels) {
                channel.removeUser(username);
                
                // Notify connected users that we're leaving.
                Set<String> users = channel.getUsers();
                for (String user: users) {
                    if (user.equalsIgnoreCase(username))
                        continue;
                    
                    ClientController userController = server.getLoggedInMap().get(user);                
                    userController.send(new ServerActionLeave(
                        userController.getNextTransactionId(),
                        channel.getName(),
                        client.getUser().getUsername()));
                }
            }
            
            server.removeLoggedIn(username);
        }
        
        try {
            this.socket.close();
        } catch (Exception e) {}
    }
    
    /**
     * A timer that automatically fires "PING" messages. 
     */
    private class TimeoutTask extends TimerTask {
        @Override
    	public void run() {
    		// Disconnect if there was no response received last time.
    		if (ClientController.this.ponged == false) {
    			ClientController.this.disconnect();
    			return;
    		}
    		
    		// Send away a ping message.
			ClientController.this.send(new ServerActionPing(
					ClientController.this.getNextTransactionId()));
			
			// Reset the ponged
			ClientController.this.ponged = false;
    	}
    }
    
    //
    // =========================================================================
    //
    
    public void pong() {
    	this.ponged = true;
    }
    
    //
    // =========================================================================
    //
    
    /**
     * Returns the next available transaction ID, incrementing the counter at
     * the same time.
     *
     * @return A free transaction ID
     */
    public int getNextTransactionId() {
    	return this.nextTransactionId++;
    }
    
    public NetworkController getNetworkController() { 
    	return this.networkController; 
	}
    
    public ClientModel getModel() { 
        return this.model; 
    }
    
    //
    // =========================================================================
    //
    
    /**
     * Constructor.
     *
     * @param controller the main network controller
     * @param socket the connected client socket
     */
    public ClientController(NetworkController networkController, Socket socket) {
        this.networkController = networkController;
        this.model = new ClientModel();
        this.socket = socket;
        
        this.awaiting = new HashMap<Integer, ServerCommand>();
        this.nextTransactionId = 0;

        // Create a timer to check for disconnections
        this.timeoutTimer = new Timer(true);
        this.ponged = true;
        
        // Try and create IO streams to the socket.
        try {
            this.out = new PrintWriter(socket.getOutputStream(), true);
            this.in = new Scanner(new BufferedReader(
                    new InputStreamReader(socket.getInputStream())));
        } catch (IOException e) {
            System.out.println("Unable to open socket IO.");
        }
    }
    
    /**
     * Thread entry point.
     */
    @Override
    public void run() {
    	// Go!
        this.timeoutTimer.scheduleAtFixedRate(new TimeoutTask(), TIMEOUT, TIMEOUT);
        
        // Loop until the socket dies.
        while (this.socket.isClosed() == false) {
            // Check if there's any data from the client.
            if (this.in.hasNextLine()) {
                String s = this.in.nextLine();
                System.out.println(s);
                
                // Try and parse the message.
                MessageParser.parseMessage(s, this);
            }
        }
        
        // stop the timer
        this.timeoutTimer.cancel();
    }
}